require 'yaml'

YAML.add_domain_type( "stadig.name,2006", "Ann::Neuron" ) do |type, val|
  n = YAML.object_maker(Ann::Neuron, val)
  n.instance_variable_set('@activation', Ann::Sigmoid)
  n.instance_variable_set('@actder', Ann::SigDer)
  n
end

module Ann
  Sigmoid = Proc.new {|i| 1 / (1 + Math.exp(-i))}
  SigDer = Proc.new {|i| 1 - (1 / (1 + Math.exp(-i)))}
  Alph = 0.3
  
  class Neuron
    attr_reader :inputs
    def initialize(*a)
      if a.size == 1 && !a[0].is_a?(Array)
        @inputs = a[0]
      elsif a.size >= 1 && a[0].is_a?(Array)
        @inputs, @weights, @bias, @activation, @actder = a
        raise "Input array is empty" if @inputs.empty?
        
        @weights = Array.new(@inputs.size).fill { Kernel.rand(0) } if @weights.nil?
        raise "Input and weight arrays must be the same size" if @inputs.size != @weights.size
        
        @bias = Kernel.rand(0).to_f if @bias.nil?
      else
        raise "Wrong number or types of arguments"
      end
      
      @activation = Sigmoid if @activation.nil?
      @actder = SigDer if @actder.nil?
      @input = nil
      @output = nil
    end
    
    def adjust(err)
      tot = @weights.inject(0) {|result, val| result + val}
      @inputs.each_index do |i|
        @weights[i] += (Alph * self.output.to_f * err * @actder.call(@input))
        @inputs[i].adjust(err * @weights[i] / tot)
      end
    end
    
    def output
      if @output.nil?
        @output = @activation.call(input)
      end
      @output
    end
    
    def input
      if @input.nil?
        if @inputs.is_a?(Array)
          @input = -1 * @bias
          @inputs.each_index do |i|
            @input += @inputs[i].output.to_f * @weights[i].to_f
          end
        else
          @input = @inputs
        end
      end
      @input
    end
    
    def clear
      @input = nil
      @output = nil
      @inputs.each do |n|
        n.clear
      end
    end
    
    def to_yaml_properties
      ["@bias", "@inputs", "@weights"]
    end
    
    def to_yaml_type
      "!stadig.name,2006/Ann::Neuron"
    end
    
    def ==(o)
      # There is some weirdness going on here.  I had to do the below to get
      # the YAML loading to work for the Network class.
      @weights.each_index do |i|
        @weights[i] == o.instance_variable_get('@weights')[i]
      end
      
      #@weights == o.instance_variable_get('@weights') &&
      @actder == o.instance_variable_get('@actder') &&
      @inputs == o.instance_variable_get('@inputs') &&
      #@bias.to_s.to_f == o.instance_variable_get('@bias') &&
      @bias.to_s.to_f == o.instance_variable_get('@bias') &&
      @activation == o.instance_variable_get('@activation')
    end
  end
end
